<template>
  <view class="scroll-list-wrap" :style="[scrollListWrapStyle]">
    <scroll-view
      class="scroll-view scroll-view-24"
      :style="[listWrapStyle]"
      scroll-y
      scroll-anchoring
      enable-back-to-top
      :scroll-top="scrollTop"
      :lower-threshold="defaultOption.lowerThreshold"
      @scroll="handleScroll"
      @touchend="handleTouchEnd"
      @touchmove.prevent.stop="handleTouchMove"
      @touchstart="handleTouchStart"
      @scrolltolower="handleScrolltolower">
      <view class="scroll-content" :style="[scrollContentStyle]">
        <view class="pull-down-wrap">
          <view class="refresh-view" :style="[refreshViewStyle]">
            <view
              class="pull-down-animation"
              :class="{ refreshing: refreshing }"
              :style="[pullDownAnimationStyle]"
            ></view>
            <text class="pull-down-text" :style="[pullDownTextStyle]">
              {{ refreshStateText }}
            </text>
          </view>
        </view>
        <view class="empty-wrap" v-if="showEmpty">
          <view class="empty-view">
            <image class="empty-image" :src="defaultOption.emptyImage" mode="aspectFit"></image>
            <text class="empty-text" :style="[emptyTextStyle]">{{ emptyText }}</text>
          </view>
        </view>
        <view class="list-content">
          <slot></slot>
        </view>
        <view class="pull-up-wrap" v-if="showPullUp">
          <view class="load-view">
            <view class="pull-up-animation" v-if="loading" :style="[pullUpAnimationStyle]"></view>
            <view class="pull-up-text" :style="[pullUpTextStyle]">{{ loadStateText }}</view>
          </view>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script setup>
import { ref, nextTick, computed, onMounted, watch, getCurrentInstance } from 'vue'
const props = defineProps({
  option: {
    type: Object,
    default: () => ({})
  }
})
const emit = defineEmits(['load', 'loadSuccess', 'loadFail', 'refresh', 'refreshSuccess', 'refreshError', 'scrolltolower', 'scroll', 'touchStart', 'touchMove', 'refreshStop', 'touchEnd'])
const defaultOption = ref({
  page: 1, // 分页
  size: 15, // 分页大小
  auto: true, // 自动加载
  height: null, // 组件高度
  disabled: false, // 禁用
  background: '', // 背景颜色属性
  emptyImage: '', // 空数据提示图片
  offsetBottom: 40, // 底部高度补偿
  pullDownSpeed: 0.5, // 下拉速率
  lowerThreshold: 40, // 距离底部上拉加载距离
  refresherThreshold: 80, // 距离顶部下拉刷新距离
  refreshDelayed: 800, // 刷新延迟
  refreshFinishDelayed: 300, // 刷新完成后的延迟
  safeArea: true, // 是否开启安全区域适配
  emptyTextColor: '#82848a', // 空提示文字颜色
  loadTextColor: '#82848a', // 上拉加载文字颜色
  loadIconColor: '#82848a', // 上拉加载图标颜色
  refresherTextColor: '#82848a', // 下拉刷新文字颜色
  refresherIconColor: '#82848a', // 下拉刷新图标颜色
  emptyText: '暂无列表~', // 空数据提示文字
  loadingText: '正在加载中~', // 加载中文字
  loadFailText: '加载失败啦~', // 加载失败文字
  noMoreText: '没有更多啦~', // 没有更多文字
  refreshingText: '正在刷新~', // 正在刷新文字
  refreshFailText: '刷新失败~', // 刷新失败文字
  refreshSuccessText: '刷新成功~', // 刷新成功文字
  pulldownText: '下拉刷新~', // 下拉中的文字
  pulldownFinishText: '松开刷新~' // 下拉完成的文字
})
const windowInfo = ref({})
const scrollTop = ref(0)
const scrollViewTop = ref(-1)
const scrollViewHeight = ref(0)
const currentPage = ref(1)
const currentSize = ref(15)
const currentScrollTop = ref(0)
const emptyText = ref('暂无列表~')
const loadStateText = ref('正在加载中~')
const refreshStateText = ref('下拉刷新~')
const loadDisabled = ref(false)
const loading = ref(false)
const refreshing = ref(false)
const refreshFinish = ref(false)
const pulldowning = ref(false)
const pullDownHeight = ref(0)
const showEmpty = ref(false)
const showPullUp = ref(false)
const showPullDown = ref(false)
const currentTouchStartY = ref(0)
const _scrollViewObserve = ref(null)
windowInfo.value = uni.getSystemInfoSync()
const _this = getCurrentInstance()
const intersectionObserver = uni.createIntersectionObserver(_this.proxy)
// 组件初始化
const handleInit = () => {
  // 合并配置
  defaultOption.value = Object.assign(defaultOption.value, props.option)
  showEmpty.value = !defaultOption.value.auto
  currentPage.value = defaultOption.value.page
  currentSize.value = defaultOption.value.size
  emptyText.value = defaultOption.value.emptyText
  loadStateText.value = defaultOption.value.loadingText
  refreshStateText.value = defaultOption.value.pulldownText
  // 计算高度
  queryRect('.scroll-view-24').then(rect => {
    // 设置组件顶部位置
    scrollViewTop.value = rect[0].top
    // 判断是否自动加载
    if (defaultOption.value.auto) load(1)
  })
}

// 加载数据
const load = () => {
  if (defaultOption.value.disabled || loading.value || loadDisabled.value) return
  // 开启正在加载
  loading.value = true
  // 设置正在加载状态文字
  loadStateText.value = defaultOption.value.loadingText
  // 显示上拉加载
  showPullUp.value = true
  // 分页参数
  const paging = { page: currentPage.value, size: currentSize.value }
  // 触发load事件
  emit('load', paging)
}
// 加载成功
const loadSuccess = (data = {}, type) => {
  // 解构数据
  const { list, total } = data
  // 判断列表是否是数组
  if (Array.isArray(list)) {
    // 判断列表长度
    if (list.length) {
      // 判断列表长度和列表总数是否相同
      if (list.length >= total) {
        // 设置禁用上拉加载
        loadDisabled.value = true
        // 加载状态文字
        loadStateText.value = defaultOption.value.noMoreText
      } else {
        // 关闭禁用上拉加载
        loadDisabled.value = false
        // 设置分页参数
        currentPage.value++
        // 加载状态为加载中
        loadStateText.value = defaultOption.value.loadingText
        // 加载计算
        loadCompute()
      }
      // 显示上拉加载
      showPullUp.value = true
      // 隐藏空数据提示
      showEmpty.value = false
    } else {
      // 设置禁用上拉加载
      loadDisabled.value = true
      // 隐藏上拉加载
      showPullUp.value = false
      // 显示空数据提示
      showEmpty.value = true
    }
    // 关闭正在加载
    loading.value = false
    // 触发加载成功事件
    emit('loadSuccess', list)
  } else {
    // 不是数组类型当作加载失败处理
    loadFail()
  }
}
// 加载失败
const loadFail = () => {
  // 关闭正在加载
  loading.value = false
  // 关闭空数据提示
  showEmpty.value = false
  // 显示上拉加载
  showPullUp.value = true
  // 加载状态为加载失败
  loadStateText.value = defaultOption.value.loadFailText
  // 触发加载失败事件
  emit('loadFail')
}
// 刷新数据
const refresh = () => {
  // 如果是下拉刷新
  if (pullDownHeight.value === defaultOption.value.refresherThreshold) {
    // 关闭正在加载
    loading.value = false
    // 隐藏上拉加载
    showPullUp.value = false
  } else {
    // 开启正在加载
    loading.value = true
    // 隐藏空数据提示
    showEmpty.value = false
    // 显示上拉加载
    showPullUp.value = true
    // 设置正在刷新状态文字
    loadStateText.value = defaultOption.value.refreshingText
  }
  // 设置刷新未完成
  refreshFinish.value = false
  // 开启正在刷新
  refreshing.value = true
  // 设置正在刷新状态文字
  refreshStateText.value = defaultOption.value.refreshingText
  // 设置分页参数
  currentPage.value = 1
  currentSize.value = defaultOption.value.size
  const paging = { page: currentPage.value, size: currentSize.value }
  // 触发refresh事件
  setTimeout(() => {
    emit('refresh', paging)
  }, defaultOption.value.refreshDelayed)
}
// 刷新成功
const refreshSuccess = (data) => {
  // 解构数据
  const { list, total } = data
  // 判断列表是否是数组
  if (Array.isArray(list)) {
    // 判断列表长度
    if (list.length) {
      // 判断列表长度和列表总数是否相同
      if (list.length >= total) {
        // 设置禁用上拉加载
        loadDisabled.value = true
        // 设置没有更多状态文字
        loadStateText.value = defaultOption.value.noMoreText
      } else {
        // 设置分页参数
        currentPage.value++
        // 关闭禁用上拉加载
        loadDisabled.value = false
        // 设置加载中状态文字
        loadStateText.value = defaultOption.value.loadingText
        // 开启自动加载
        defaultOption.value.auto = true
        // 加载计算
        loadCompute()
      }
      // 关闭空数据提示
      showEmpty.value = false
      // 显示上拉加载
      showPullUp.value = true
    } else {
      // 设置禁用上拉加载
      loadDisabled.value = true
      // 隐藏上拉加载
      showPullUp.value = false
      // 显示空数据提示
      showEmpty.value = true
      // 设置没有更多状态文字
      loadStateText.value = defaultOption.value.noMoreText
    }
    // 关闭正在加载
    loading.value = false
    // 设置刷新成功状态文字
    refreshStateText.value = defaultOption.value.refreshSuccessText
    // 关闭正在刷新
    refreshing.value = false
    // 关闭正在下拉
    pulldowning.value = false
    // 触发刷新成功事件
    emit('refreshSuccess', list)
    setTimeout(() => {
      // 设置刷新完成
      refreshFinish.value = true
      // 重置下拉高度
      pullDownHeight.value = 0
      // 隐藏下拉刷新
      showPullDown.value = false
      emit('refreshSuccess')
    }, defaultOption.value.refreshFinishDelayed)
  } else {
    // 不是数组类型当作刷新失败处理
    refreshFail()
  }
}
// 刷新失败
const refreshFail = () => {
  // 设置加载失败状态文字
  loadStateText.value = defaultOption.value.refreshFailText
  // 设置刷新失败状态文字
  refreshStateText.value = defaultOption.value.refreshFailText
  // 关闭正在加载
  loading.value = false
  // 显示下拉加载
  showPullUp.value = true
  // 关闭正在刷新
  refreshing.value = false
  // 关闭正在下拉
  pulldowning.value = false
  // 延迟执行刷新完成后状态
  setTimeout(() => {
    // 设置刷新完成
    refreshFinish.value = true
    // 重置下拉高度
    pullDownHeight.value = 0
    // 隐藏下拉刷新
    showPullDown.value = false
    // 触发刷新失败事件
    emit('refreshError')
  }, defaultOption.value.refreshFinishDelayed)
}
// 加载计算
const loadCompute = (type) => {
  // 判断是否自动加载
  if (defaultOption.value.auto) {
    // 延迟执行下否者可能会高度计算错误
    setTimeout(() => {
      nextTick(() => {
        queryRect('.list-content').then(rect => {
          if (rect[0].height <= scrollViewHeight.value) {
            load()
          }
        })
      })
    }, 100)
  }
}
// 上拉触底事件
const handleScrolltolower = (e) => {
  if (loadDisabled.value) return
  emit('scrolltolower', e)
  load()
}
// 滚动事件
const handleScroll = (event) => {
  currentScrollTop.value = event.detail.scrollTop
  emit('scroll', event.detail)
}
// 触摸按下处理
const handleTouchStart = (event) => {
  if (defaultOption.value.disabled) return
  currentTouchStartY.value = event.touches[0].clientY
  emit('touchStart', event)
}
// 触摸按下滑动处理
const handleTouchMove = (event) => {
  if (defaultOption.value.disabled || currentScrollTop.value) return
  if (event.touches[0].clientY >= currentTouchStartY.value) {
    pulldowning.value = true
    showPullDown.value = true
    const pullDownDistance = (event.touches[0].clientY - currentTouchStartY.value) * defaultOption.value.pullDownSpeed
    pullDownHeight.value =
      pullDownDistance > defaultOption.value.refresherThreshold
        ? defaultOption.value.refresherThreshold
        : pullDownDistance
    refreshStateText.value =
      pullDownHeight.value >= defaultOption.value.refresherThreshold
        ? defaultOption.value.pulldownFinishText
        : defaultOption.value.pulldownText
    emit('touchMove', event)
  }
}
// 触摸松开处理
const handleTouchEnd = (event) => {
  if (defaultOption.value.disabled) return
  // 当下拉高度小于下拉阈值
  if (pullDownHeight.value < defaultOption.value.refresherThreshold) {
    // 关闭正在下拉
    pulldowning.value = false
    // 重置下拉高度
    pullDownHeight.value = 0
    // 隐藏下拉刷新
    showPullDown.value = false
    // 触发下拉中断事件
    emit('refreshStop')
  } else {
    refresh()
  }
  // 触发下拉触摸松开事件
  emit('touchEnd', event)
}
// 更新组件
const updateScrollView = () => {
  if (defaultOption.value.height) {
    scrollViewHeight.value = uni.upx2px(defaultOption.value.height)
  } else {
    scrollViewHeight.value = windowInfo.value.windowHeight - scrollViewTop.value
  }
  scrollViewObserve()
}
// 监听组件位置变化
const scrollViewObserve = () => {
  disconnectObserve('_scrollViewObserve')
  nextTick(() => {
    queryRect('.scroll-view-24').then(rect => {
      if (!rect[0]) return
      intersectionObserver.relativeToViewport({
        top: -rect[0].top
      })?.observe('.scroll-view-24', position => {
        scrollViewTop.value = position.boundingClientRect.top
      })
      _scrollViewObserve.value = scrollViewObserve
    })
  })
}
// 断开监听组件
const disconnectObserve = (observerName) => {
  const observer = [observerName].value
  observer && observer.disconnect()
}
// 查询dom节点信息
const queryRect = (selector, all) => {
  return new Promise(resolve => {
    let query
    // #ifdef H5
    query = uni.createSelectorQuery()
    // #endif
    // #ifdef MP-WEIXIN
    query = _this.proxy.createSelectorQuery()
    // #endif
    query.select(selector).boundingClientRect()
    query.exec(rect => {
      resolve(rect)
    })
  })
}
// 16进制转RGB
const hexToRgb = (hex) => {
  const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
  hex = hex.replace(shorthandRegex, (m, r, g, b) => {
    return r + r + g + g + b + b
  })
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
  return result
    ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      }
    : null
}
const scrollListWrapStyle = computed(() => {
  const style = {}
  style.background = defaultOption.value.background
  return style
})
// 组件容器样式
const listWrapStyle = computed(() => {
  const style = {}
  const { offsetBottom } = defaultOption.value
  style.height = scrollViewHeight.value - uni.upx2px(offsetBottom) + 'px'
  return style
})
// 滚动内容样式
const scrollContentStyle = computed(() => {
  const style = {}
  style.transform = showPullDown.value ? `translate(0px, ${pullDownHeight.value}px) translateZ(0px)` : 'translate(0px, 0px) translateZ(0px)'
  style.transition = pulldowning.value ? 'transform 100ms ease-out' : 'transform 200ms linear'
  return style
})
// 下拉刷新样式
const refreshViewStyle = computed(() => {
  const style = {}
  style.opacity = showPullDown.value ? 1 : 0
  return style
})
// 下拉中动画样式
const pullDownAnimationStyle = computed(() => {
  const style = {}
  const { refresherIconColor, refresherThreshold } = defaultOption.value
  const { r, g, b } = hexToRgb(refresherIconColor)
  const rate = pullDownHeight.value / refresherThreshold
  style.borderColor = `rgba(${r},${g},${b},0.2)`
  style.borderTopColor = refresherIconColor
  if (!refreshing.value) {
    style.transform = `rotate(${360 * rate}deg)`
    style.transition = 'transform 100ms linear'
  }
  return style
})

const pullDownTextStyle = computed(() => {
  const style = {}
  const { refresherTextColor } = defaultOption.value
  style.color = refresherTextColor
  return style
})

// 上拉中动画样式
const pullUpAnimationStyle = computed(() => {
  const style = {}
  const { loadIconColor } = defaultOption.value
  const { r, g, b } = hexToRgb(loadIconColor)
  style.borderColor = `rgba(${r},${g},${b},0.2)`
  style.borderTopColor = loadIconColor
  return style
})
// 上拉中文字样式
const pullUpTextStyle = computed(() => {
  const style = {}
  const { loadTextColor } = defaultOption.value
  style.color = loadTextColor
  return style
})
// 空数据提示文字样式
const emptyTextStyle = computed(() => {
  const style = {}
  const { emptyTextColor } = defaultOption.value
  style.color = emptyTextColor
  return style
})
watch(
  () => scrollViewTop.value,
  (newValue, oldValue) => {
    updateScrollView()
  }
)
onMounted(() => {
  handleInit()
})

defineExpose({
  loadSuccess,
  refreshSuccess
})
</script>

<style scoped lang="scss">
.scroll-list-wrap {
  box-sizing: border-box;
  .scroll-view {
    position: relative;
    // padding-bottom: constant(safe-area-inset-bottom);
    // padding-bottom: env(safe-area-inset-bottom);
    .scroll-content {
      height: 100%;
      display: flex;
      will-change: transform;
      flex-direction: column;
      .pull-down-wrap {
        left: 0;
        width: 100%;
        display: flex;
        padding: 30rpx 0;
        position: absolute;
        align-items: flex-end;
        justify-content: center;
        transform: translateY(-100%);
        .refresh-view {
          display: flex;
          justify-content: center;
          transition: all .5s;
          .pull-down-animation {
            width: 32rpx;
            height: 32rpx;
            border-width: 4rpx;
            border-style: solid;
            border-radius: 50%;
            &.refreshing {
              animation: spin 0.5s linear infinite;
            }
            @keyframes spin {
              to {
                transform: rotate(360deg);
              }
            }
          }
          .pull-down-text {
            margin-left: 10rpx;
          }
        }
      }
      .empty-wrap {
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        display: flex;
        position: absolute;
        align-items: center;
        flex-direction: column;
        .empty-view {
          margin: auto;
          display: flex;
          align-items: center;
          flex-direction: column;
          .empty-image {
            width: 386rpx;
            height: 326rpx;
          }
          .empty-text {
            color: #606266;
            margin-top: 20rpx;
          }
        }
      }
      .pull-up-wrap {
        height: 60rpx;
        .load-view {
          padding: 10rpx 0;
          display: flex;
          align-items: center;
          justify-content: center;
          .pull-up-animation {
            width: 32rpx;
            height: 32rpx;
            border-width: 4rpx;
            border-style: solid;
            border-radius: 50%;
            animation: spin 0.5s linear infinite;
          }
          .pull-up-text {
            margin-left: 10rpx;
          }
        }
      }
    }
  }
}
</style>
